package org.kca.yarmour.data;

import java.util.Arrays;
import java.util.List;
import java.util.ArrayList;

public class NotificationList
{
    private int categories, numFilters;
    private boolean[] filter;
    private int[] size, last;
    private List<Notification> list;
    
    public NotificationList(int categories)
    {
        if (categories <= 0 || categories > 8) 
        {
            throw new IllegalArgumentException("Number of categories not supported");
        }
        this.categories = categories;
        list = new ArrayList<Notification>();
        size = new int[categories];
        last = new int[categories];
        filter = new boolean[categories];
        numFilters = categories;
        Arrays.fill(filter, true);
        Arrays.fill(size, 0);
        Arrays.fill(last, -1);
    }
    
    /**
     * Adds a notification to the list
     * @param data the notification message
     * @param category the category
     */
    public void addNotification(String data, int category)
    {
        Notification n = new Notification(data, category, categories);
        n.index = size[category];
        n.prev = last[category];
        size[category]++;
        for (int i = 0; i < categories; i++)
        {
            if (last[i] == -1)
            {
                continue;
            }
            Notification n2 = list.get(last[i]);
            if (n2.next[category] == -1)
            {
                n2.next[category] = list.size();
                while (n2.prev != -1 && list.get(n2.prev).next[category] == -1)
                {
                    n2 = list.get(n2.prev);
                    n2.next[category] = list.size();
                }
            }
        }
        last[category] = list.size();
        list.add(n);
    }
    
    /**
     * Toggles a particular category in the filter
     * @param category the category to toggle
     * @param on set to true to toggle on, false to toggle off
     */
    public void toggleFilter(int category, boolean on)
    {
        if (category >= 0 && category < categories && filter[category] != on)
        {
            if (on)
            {
                filter[category] = true;
                numFilters++;
            }
            else if (numFilters > 1)
            {
                filter[category] = false;
                numFilters--;
            }
        }
    }
    
    /**
     * Toggles the state of a category in the filter
     * @param category the category to toggle
     */
    public void toggleFilter(int category)
    {
        if (category >= 0 && category < categories)
        {
            if (filter[category])
            {
                if (numFilters > 1)
                {
                    filter[category] = false;
                    numFilters--;
                }
            }
            else
            {
                filter[category] = true;
                numFilters++;
            }
        }
    }
    
    /**
     * Turns on all filters
     */
    public void allFiltersOn()
    {
        Arrays.fill(filter, true);
        numFilters = categories;
    }
    
    /**
     * Gets the total number of all notifications
     * @return the total number of notifications
     */
    public int getTotalSize()
    {
        return list.size();
    }
    
    /**
     * Gets the number of notifications in the filtered list
     * @return the number of filtered notifications
     */
    public int getSize()
    {
        int sum = 0;
        for (int i = 0; i < categories; i++)
        {
            if (filter[i])
            {
                sum += size[i];
            }
        }
        return sum;
    }
    
    /**
     * Gets the notification in the filtered list at a particular index
     * @param index the index to get
     * @return the notification at that index, or null if it doesnt exist
     */
    public Notification get(int index)
    {
        int idx = getTrueIndex(index);
        if (idx == -1)
        {
            return null;
        }
        return list.get(idx);
    }
    
    /**
     * Gets the true index in the array, given the perceived index in the filtered list
     * @param index the perceived index in the filtered list
     * @return the true index in the array corresponding to the perceived index
     */
    public int getTrueIndex(int index)
    {
        int sz = getSize();
        if (index < 0 || index >= sz) 
        {
            return -1;
        }
        int l = 0;
        int r = list.size() - 1;
        while (l < r)
        {
            int m = (l + r) / 2;
            int si = subIndex(m);
            if (si < index)
            {
                l = m + 1;
            }
            else if (si > index)
            {
                r = m - 1;
            }
            else
            {
                if (filter[list.get(m).category])
                {
                    return m;
                }
                else
                {
                    r = m - 1;
                }
            }
        }
        return l;
    }
    
    /**
     * Gets the perceived index of the element at the real index specified. Note that
     * the element at this index is only equal to that returned by get() if it is
     * in the filter
     * @param index the real index
     * @return the perceived index
     */
    private int subIndex(int index)
    {
        int sum = 0;
        Notification n = list.get(index);
        for (int i = 0; i < categories; i++)
        {
            if (!filter[i])
            {
                continue;
            }
            if (n.next[i] != -1)
            {
                sum += list.get(n.next[i]).index;
            }
            else
            {
                sum += size[i];
            }
        }
        return sum - 1;
    }
    
    /**
     * Gets the next valid perceived index in the filtered list given the true index
     * @param trueIndex the true index in the actual list
     * @return the next valid index
     */
    public int getValidIndex(int trueIndex)
    {
        int sum = 0;
        Notification n = list.get(trueIndex);
        for (int i = 0; i < categories; i++)
        {
            if (!filter[i])
            {
                continue;
            }
            if (n.next[i] != -1)
            {
                sum += list.get(n.next[i]).index;
            }
            else
            {
                sum += size[i];
            }
        }
        if (sum == 0)
        {
            return 0;
        }
        else if (filter[get(sum - 1).category])
        {
            return sum - 1;
        }
        else
        {
            return nextIndex(sum - 1);
        }
    }
    
    /**
     * Given a current perceived index, gets the next perceived index in the filter
     * @param index the current perceived index
     * @return the next perceived index
     */
    private int nextIndex(int index)
    {
        int best = -1;
        Notification n = get(index);
        for (int i = 0; i < categories; i++)
        {
            if (filter[i])
            {
                int ni = n.next[i];
                if (ni != -1)
                {
                    if (best == -1 || ni < best)
                    {
                        best = ni;
                    }
                }
            }
        }
        return best;
    }
    
    public static class Notification
    {
        private int index, prev;
        private int[] next;
        private int category;
        private String data;
        
        public Notification(String data, int category, int numCategories)
        {
            this.data = data;
            this.category = category;
            next = new int[numCategories];
            Arrays.fill(next, -1);
        }
        
        public String getData()
        {
            return data;
        }
        
        public int getCategory()
        {
            return category;
        }
    }
}
